天色突然暗了下來,一股詭譎感瀰漫,令人不禁冒出冷汗。
還好,隨身攜帶頭燈可是探險家的必備要領。
山姆把頭燈戴上,整座黑森林裡只看得見山姆一個人。
「必須下山了!終點應該快到了吧!」
PS. 這裡是開發 iOS 手機遊戲的系列文,如果還沒看過之前
劇情文章的朋友,歡迎先點這邊回顧唷!
我們一樣將偵測的邏輯寫在 update 裡,將 crystals 及 magicCrystals 使用 filter 過濾出還沒有被收集的 水晶/魔幻水晶,並使用 isComplete 參數來判斷是否已經進入破關狀態,接著執行破關方法: gameComplete
gameComplete 中寫入要做的動作:
stopTimer():將所有計時器關閉setCanMove(isCanMove: false):讓主角及怪物設定為不能移動playMusicByName:播放遊戲破關音樂 FinishMusic,約 6 秒gameNextLevel
gameNextLevel:
resetPosition:主角及怪物設定回原來的位置setMode(mode: .ATTACK):將怪物的模式設定回攻擊setGotten(isGotten:false):收集物皆設定回尚未收集的狀態level 加 1isComplete 設定回 false
gameStart 方法,讓遊戲開始class GameScene: SKScene {
    ...
    var isComplete: Bool = false
    var level: Int = 1
    override func update(_ currentTime: TimeInterval) {
        ...
        if self.crystals.filter({!$0.isGotten}).count == 0 && self.magicCrystals.filter({!$0.isGotten}).count == 0 && !self.isComplete {
            self.isComplete = true
            self.gameComplete()
        }
    }
    func gameComplete() {
        self.stopTimer()
        if let sam = self.sam {
            sam.setCanMove(isCanMove: false)
        }
        for weather in weathers {
            weather.setCanMove(isCanMove: false)
        }
        self.playMusicByName(musicName: "FinishMusic")
        Timer.scheduledTimer(timeInterval: 6, target: self, selector: #selector(gameNextLevel), userInfo: nil, repeats: false)
    }
    @objc func gameNextLevel() {
        for weather in self.weathers {
            weather.resetPosition()
            weather.setMode(mode: .ATTACK)
        }
        if let sam = self.sam {
            sam.resetPosition()
        }
        for crystal in self.crystals {
            crystal.setGotten(isGotten:false)
        }
        for magicCrystal in self.magicCrystals {
            magicCrystal.setGotten(isGotten:false)
        }
        for mushroom in self.mushrooms {
            mushroom.setGotten(isGotten:false)
        }
        self.level += 1
        self.isComplete = false
        
        self.gameStart()
    }
}
levelLabel,用來顯示當前的等級,顯示文字設定為 Level: \(self.level)。將文字的顏色 (fontColor)、大小 (fontSize)、字體 (fontName)、垂直對齊 (verticalAlignmentMode)、水平對齊 (horizontalAlignmentMode) 分別設定好,將等級文字節點加到場景中applySafeArea 方法中,校正等級文字節點的位置,方法跟之前使用的一樣gameNextLevel 方法,進入下一關時,更新等級的文字顯示class GameScene: SKScene {
    ...
    var levelLabel: SKLabelNode?
    
    override func didMove(to view: SKView) {
        ...
        self.levelLabel = SKLabelNode(text: "Level: \(self.level)")
        if let levelLabel = self.levelLabel {
            levelLabel.fontColor = UIColor.white
            levelLabel.fontSize = CGFloat(22)
            levelLabel.fontName = "Copperplate"
            levelLabel.verticalAlignmentMode = .center
            levelLabel.horizontalAlignmentMode = .right
            self.addChild(levelLabel)
        }
    }
    func applySafeArea() {
        ...
        if let mapNode = self.mapNode, let scoreNode = self.scoreNode, let lifeNode = self.lifeNode, let levelLabel = self.levelLabel {
            mapNode.position = CGPoint(x: 0, y: -self.topSafeArea - scoreNode.size.height)
            scoreNode.position =  CGPoint(x: 0 ,y: -self.topSafeArea - scoreNode.size.height/2)
            lifeNode.position = CGPoint(x: 0, y: -self.topSafeArea - scoreNode.size.height - mapNode.size.height - 15)
            levelLabel.position = CGPoint(x: self.size.width - 10, y: -self.topSafeArea - scoreNode.size.height - mapNode.size.height - 15)
        }
    }
    @objc func gameNextLevel() {
        ...
        self.levelLabel!.text = "Level: \(self.level)"
    }
}
clearLabel,放在畫面的中央,文字設定為 Clear!
alpha 為 0,先暫時不顯示mapNode 中5 次class GameScene: SKScene {
    ...
    var clearLabel: SKLabelNode?
    
    override func didMove(to view: SKView) {
        ...
        self.clearLabel = SKLabelNode(text: "Clear!")
        if let clearLabel = self.clearLabel {
            clearLabel.fontColor = UIColor.white
            clearLabel.fontSize = CGFloat(22)
            clearLabel.fontName = "Copperplate"
            clearLabel.position = CGPoint(x: self.gridWH * 8 + gridWH/2, y: -gridWH * 12 - gridWH/2);
            clearLabel.verticalAlignmentMode = .center
            clearLabel.horizontalAlignmentMode = .center
            clearLabel.zPosition = 5
            clearLabel.alpha = 0
            self.mapNode!.addChild(clearLabel)
        }
    }
    func gameComplete() {
        ...
        let ani1 = SKAction.fadeAlpha(to: 1, duration: 0.6)
        let ani2 = SKAction.fadeAlpha(to: 0, duration: 0.3)
        let aniAlpha = SKAction.sequence([ani1, ani2])
        let aniRepeat = SKAction.repeat(aniAlpha, count: 5)
        self.clearLabel!.run(aniRepeat)
    }
}
破關後,遊戲中間會出現 Clear! 的文字,接著遊戲重新設定,並且可以看到右下角的等級上升。
但目前破關後沒有任何的改變,我們接著來為它增加下一關的難度。
我們先製作第 2 關以後的遊戲場景,都具有聚光燈的效果
可以想像成遊戲畫面會變成黑色,只有放上光節點的地方會被照亮,我們讓這個光源跟著主角一起移動,製造出像是在主角頭上打了一盞聚光燈的感覺
使用 SKLightNode 類別可以新增一個燈光實體,用來照亮附近的節點。
有三種顏色屬性可以設定:
接著我們來做設定:
addLight()
light
ambientColor 及 lightColor 的顏色categoryBitMask 設定為 1,設定燈光的類型為 1
falloff:光源的衰減率指數,設定為 1
class GameScene: SKScene {
    ...
    var light: SKLightNode?
    func addLight() {
        if self.light == nil {
            self.light = SKLightNode()
            self.light!.ambientColor = UIColor(red: 50/255, green: 50/255, blue: 50/255, alpha: 0.3)
            self.light!.lightColor = UIColor(red: 250/255, green: 250/255, blue: 250/255, alpha: 0.8)
            self.light!.categoryBitMask = 1
            self.light!.falloff = 1
            self.mapNode!.addChild(self.light!)
        }
    }
}
我們希望迷宮中的所有東西,包含主角、怪物、地圖、水晶等收集物都能有光照的效果,因此可以透過設定節點的 lightingBitMask 的值,讓它跟燈光的 categoryBitMask 屬性值一樣,就可以產生被光源照射的效果
lightingBitMask 值設定為 1
class GameScene: SKScene {
    ...
    override func didMove(to view: SKView) {
        ...
        self.sam!.node.lightingBitMask = 1
        rain.node.lightingBitMask = 1
        storm.node.lightingBitMask = 1
        lightning.node.lightingBitMask = 1
        snow.node.lightingBitMask = 1
    }
    func drawMap() {
        for i in 0..<gridYCount {
            let mapRowArr = Array(mapDraw[i]);
            for j in 0..<gridXCount {
                let mapKeys = wallMapping.keys
                switch mapRowArr[j] {
                case _ where mapKeys.contains(mapRowArr[j]):
                    let spriteItem = SKSpriteNode(imageNamed: wallMapping[mapRowArr[j]]!)
                    ...
                    spriteItem.lightingBitMask = 1
                case "+":
                    let mushroom = Collection(gridWH: self.gridWH, gridX: j, gridY: i, imageName: "mushroom")
                    ...
                    mushroom.node.lightingBitMask = 1
                case ".":
                    let crystal = Collection(gridWH: self.gridWH, gridX: j, gridY: i, imageName: "crystal")
                    ...
                    crystal.node.lightingBitMask = 1
                case "*":
                    let magicalCrystal = MagicalCrystal(gridWH: self.gridWH, gridX: j, gridY: i, imageName: "magical-crystal")
                    ...
                    magicalCrystal.node.lightingBitMask = 1
                default:
                    break
                }
            }
        }
    }
}
寫好新增燈光節點 (SKLightNode) 的方法後,我們在遊戲進入下一關的時候呼叫它: addLight()
class GameScene: SKScene {
    ...
    @objc func gameNextLevel() {
        self.addLight()
        ...
    }
}
為了讓燈光能跟著主角移動,在 update 中,判斷遊戲如果已經有新增燈光節點的話,就改動它的位置 position,讓它跟主角的位置一樣
class GameScene: SKScene {
    ...
    override func update(_ currentTime: TimeInterval) {
        ...
        if let light = self.light {
            light.position = sam.node.position
        }
    }
}
在遊戲結束時,將燈光節點移除 removeChildren(in: [light])
class GameScene: SKScene {
    ...
    @objc func gameOver() {
        ...
        if let light = self.light {
            self.mapNode!.removeChildren(in: [light])
        }
        ...
    }
}
可以看到遊戲破關後,開始有場景燈光效果,也會跟著主角移動
聚光燈效果
只有主角附近有燈光,其他會隨著範圍越遠而看不到,更有黑森林的感覺了!

大家可以試著優化,將玩家的最高關卡等級也紀錄到本機中,並且顯示在遊戲結束的畫面上。
這邊因為篇幅有限的關係,只介紹第 2 關以後的關卡加上聚光燈效果來提升難度,大家可以再繼續讓後面關卡的地圖,樣子長得跟之前的關卡都不一樣,增加多樣性。
參考來源:
repeat(_:count:)
SKLightNode
ambientColor
lightColor
shadowColor
categoryBitMask
falloff
lightingBitMask